package org.zoomdev.zoom.dao.ddl;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.zoomdev.zoom.dao.DaoException;
import org.zoomdev.zoom.dao.annotations.Table;
import org.zoomdev.zoom.dao.ddl.utils.DDLClassParser;
import org.zoomdev.zoom.dao.meta.ColumnMeta;
import org.zoomdev.zoom.dao.meta.IndexMeta;
import org.zoomdev.zoom.dao.meta.TableMeta;
import org.zoomdev.zoom.dao.modules.CoreDaoModule;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;

public final class DatabaseBuilderImpl implements DatabaseBuilder {
    private static final Logger log = LoggerFactory.getLogger(CoreDaoModule.NAME);

    private final DataSource dataSource;
    private ArrayList<CreateCommand> commands = new ArrayList<>();

    private CreateTable createTable;


    public DatabaseBuilderImpl(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    @Override
    public DatabaseBuilder dropIfExists(String table) {
        commands.add(new DropTable(table));
        return this;
    }

    @Override
    public DatabaseBuilder createIfNotExists(String table) {
        createTable = new CreateTable(table, true);
        commands.add(createTable);
        return this;
    }

    @Override
    public DatabaseBuilder createIfNotExists(TableMeta table) {
        commands.add(new CreateTableMeta(table));
        return this;
    }

    @Override
    public DatabaseBuilder createIfNotExists(Class<?> table) {
        createIfNotExists(DDLClassParser.parse(table));
        return this;
    }

    @Override
    public DatabaseBuilder dropIfExists(Class<?> table) {
        String tableName = null;
        _DDL_Table dt = table.getAnnotation(_DDL_Table.class);
        if (dt != null) {
            tableName = dt.name();
        } else {
            Table tbl = table.getAnnotation(Table.class);
            if (tbl != null) {
                tableName = tbl.value();
            }
        }
        if (tableName == null) {
            throw new DaoException("找不到" + table.getName() + "的表，无标注");
        }

        commands.add(new DropTable(tableName));
        return this;
    }

    @Override
    public DatabaseBuilder createTable(String table) {
        createTable = new CreateTable(table, false);
        commands.add(createTable);
        return this;
    }

    @Override
    public DatabaseBuilder comment(String comment) {
        if (createTable.currentColumn != null) {
            createTable.currentColumn.setComment(comment);
        } else {
            createTable.tableComment = comment;
        }
        return this;
    }

    @Override
    public DatabaseBuilder add(String column) {
        createTable.currentColumn = new ColumnMeta();
        createTable.currentColumn.setName(column);
        createTable.columns.add(createTable.currentColumn);
        return this;
    }

    @Override
    public DatabaseBuilder autoIncrement() {
        Objects.requireNonNull(createTable.currentColumn, "currentColumn不能为null");
        createTable.currentColumn.setAutoIncrement(true);
        return this;
    }

    @Override
    public DatabaseBuilder index() {
        String column = createTable.currentColumn.getName();
        createCompoundIndex("idx_" + column, column);
        return this;
    }

    @Override
    public DatabaseBuilder primaryKey() {
        createCompoundPrimary(createTable.currentColumn.getName());
        return this;
    }

    @Override
    public DatabaseBuilder uniqueKey() {
        String column = createTable.currentColumn.getName();
        createCompoundUnique("uni_" + column, column);
        return this;
    }

    @Override
    public DatabaseBuilder notNull() {
        createTable.currentColumn.setNotNull(true);
        return this;
    }

    @Override
    public DatabaseBuilder varchar(int size) {
        setColumnType(SqlType.VARCHAR);
        Objects.requireNonNull(createTable.currentColumn, "currentColumn不能为null");
        createTable.currentColumn.setSize(size);
        return this;
    }

    @Override
    public DatabaseBuilder charString(int size) {
        setColumnType(SqlType.CHAR);
        createTable.currentColumn.setSize(size);
        return this;
    }

    @Override
    public DatabaseBuilder smallInt() {
        setColumnType(SqlType.SMALLINT);
        return this;
    }

    @Override
    public DatabaseBuilder smallIntUnsigned() {
        setColumnType(SqlType.SMALLINT_UNSIGNED);
        return this;
    }

    @Override
    public DatabaseBuilder text() {
        setColumnType(SqlType.TEXT);
        return this;
    }

    @Override
    public DatabaseBuilder timestamp() {
        setColumnType(SqlType.TIMESTAMP);
        return this;
    }

    @Override
    public DatabaseBuilder datetime() {
        setColumnType(SqlType.DATETIME);
        return this;
    }

    @Override
    public DatabaseBuilder date() {
        setColumnType(SqlType.DATE);
        return this;
    }

    @Override
    public DatabaseBuilder integer() {
        setColumnType(SqlType.INT);
        return this;
    }

    @Override
    public DatabaseBuilder mediumint() {
        setColumnType(SqlType.MEDIUMINT);
        return this;
    }

    @Override
    public DatabaseBuilder integerUnsigned() {
        setColumnType(SqlType.INT_UNSIGNED);
        return this;
    }

    @Override
    public DatabaseBuilder bigInt() {
        setColumnType(SqlType.BIGINT);
        return this;
    }

    @Override
    public DatabaseBuilder bigIntUnsigned() {
        setColumnType(SqlType.BIGINT_UNSIGNED);
        return this;
    }

    @Override
    public DatabaseBuilder tinyInt() {
        setColumnType(SqlType.TINYINT);
        return this;
    }

    @Override
    public DatabaseBuilder tinyIntUnsigned() {
        setColumnType(SqlType.TINYINT_UNSIGNED);
        return this;
    }

    @Override
    public DatabaseBuilder tinyblob() {
        setColumnType(SqlType.TINYBLOB);
        return this;
    }

    @Override
    public DatabaseBuilder blob() {
        setColumnType(SqlType.BLOB);
        return this;
    }

    @Override
    public DatabaseBuilder mediumBlob() {
        setColumnType(SqlType.MEDIUMBLOB);
        return this;
    }

    @Override
    public DatabaseBuilder clob() {
        setColumnType(SqlType.CLOB);
        return this;
    }

    @Override
    public DatabaseBuilder decimal(int precision, int scale) {
        setColumnType(SqlType.DECIMAL);

        Objects.requireNonNull(createTable.currentColumn, "currentColumn不能为null");
        createTable.currentColumn.setSize(precision);
        createTable.currentColumn.setScale(scale);
        return this;
    }

    @Override
    public DatabaseBuilder real4(int size, int scale) {
        setColumnType(SqlType.FLOAT);
        createTable.currentColumn.setSize(size);
        createTable.currentColumn.setScale(scale);
        return this;
    }

    @Override
    public DatabaseBuilder real8(int size, int scale) {
        setColumnType(SqlType.DOUBLE);
        createTable.currentColumn.setSize(size);
        createTable.currentColumn.setScale(scale);
        return this;
    }

    @Override
    public DatabaseBuilder real4() {
        setColumnType(SqlType.FLOAT);
        return this;
    }

    @Override
    public DatabaseBuilder real8() {
        setColumnType(SqlType.DOUBLE);
        return this;
    }

    @Override
    public DatabaseBuilder engine(String engine) {
        createTable.engine = engine;
        return this;
    }

    @Override
    public DatabaseBuilder createCompoundIndex(String indexName, String... columns) {
        IndexMeta index = new IndexMeta();
        index.setName(indexName);
        index.setColumns(columns);
        createTable.indices.add(index);
        return this;
    }

    @Override
    public DatabaseBuilder createCompoundUnique(String indexName, String... columns) {
        IndexMeta index = new IndexMeta();
        index.setName(indexName);
        index.setColumns(columns);
        index.setUnique(true);
        createTable.indices.add(index);
        return this;
    }

    @Override
    public DatabaseBuilder createCompoundPrimary(String... columns) {
        createTable.primaryKeys.addAll(Arrays.asList(columns));
        return this;
    }

    @Override
    public List<String> buildSql() {
        List<String> list = new ArrayList<>();
        for (CreateCommand command : commands) {
            String sql = command.getSql(true);
            list.add(sql);
        }
        return list;
    }

    @Override
    public void build() {
        try {
            try (Connection connection = dataSource.getConnection()) {
                try (Statement statement = connection.createStatement()) {
                    for (CreateCommand command : commands) {
                        String sql = command.getSql(false);
                        log.info(sql);
                        statement.executeUpdate(sql);
                    }
                }
            }
        } catch (Exception e) {
            throw new DaoException(e);
        }

    }

    @Override
    public List<TableMeta> buildTableMetas() {
        List<TableMeta> list = new ArrayList<>();
        for (CreateCommand command : commands) {
            if (command instanceof CreateTable) {
                CreateTable cr = (CreateTable) command;
                list.add(cr.buildTableMeta());
            }
        }
        return list;
    }

    @Override
    public DatabaseBuilder time() {
        setColumnType(SqlType.TIME);
        return this;
    }

    @Override
    public DatabaseBuilder year() {
        setColumnType(SqlType.YEAR);
        return this;
    }

    @Override
    public DatabaseBuilder binary(int size) {
        setColumnType(SqlType.BINARY);
        createTable.currentColumn.setSize(size);
        return this;
    }

    @Override
    public DatabaseBuilder varbinary(int size) {
        setColumnType(SqlType.VARBINARY);
        createTable.currentColumn.setSize(size);
        return this;
    }

    @Override
    public DatabaseBuilder tinytext() {
        setColumnType(SqlType.TINYTEXT);
        return this;
    }

    @Override
    public DatabaseBuilder mediumtext() {
        setColumnType(SqlType.MEDIUMTEXT);
        return this;
    }

    @Override
    public DatabaseBuilder mediumblob() {
        setColumnType(SqlType.MEDIUMBLOB);
        return this;
    }

    @Override
    public DatabaseBuilder longblob() {
        setColumnType(SqlType.LONGBLOB);
        return this;
    }

    @Override
    public DatabaseBuilder bool() {
        setColumnType(SqlType.BOOLEAN);
        return this;
    }

    @Override
    public DatabaseBuilder longtext() {
        setColumnType(SqlType.LONGTEXT);
        return this;
    }

    @Override
    public DatabaseBuilder json() {
        setColumnType(SqlType.JSON);
        return this;
    }

    @Override
    public DatabaseBuilder enumString(String... values) {
        setColumnType(SqlType.ENUM);
        createTable.currentColumn.setValues(values);
        return this;
    }

    @Override
    public DatabaseBuilder setString(String... values) {
        setColumnType(SqlType.SET);
        createTable.currentColumn.setValues(values);
        return this;
    }

    @Override
    public DatabaseBuilder defaultValue(String defaultValue) {
        createTable.currentColumn.setDefaultValue(defaultValue);
        return this;
    }

    @Override
    public DatabaseBuilder string(int size) {
        varchar(size);
        return this;
    }

    // Column type definitions
    private void setColumnType(SqlType type) {
        Objects.requireNonNull(createTable.currentColumn, "currentColumn不能为null");
        createTable.currentColumn.setType(type);
    }

    interface CreateCommand {
        String getSql(boolean pretty);
    }

    static class DropTable implements CreateCommand {

        private final String table;

        DropTable(String table) {
            this.table = table;
        }

        @Override
        public String getSql(boolean pretty) {
            return "DROP TABLE IF EXISTS " + table;
        }
    }

    static class CreateTableMeta implements CreateCommand {

        private final TableMeta tableMeta;

        CreateTableMeta(TableMeta tableMeta) {
            this.tableMeta = tableMeta;
        }

        @Override
        public String getSql(boolean pretty) {
            return tableMeta.toSql(true, pretty);
        }
    }

    static class CreateTable implements CreateCommand {

        private final String table;
        private final List<ColumnMeta> columns = new ArrayList<>();
        private final List<IndexMeta> indices = new ArrayList<>();
        private final List<String> primaryKeys = new ArrayList<>();
        private final boolean ifNotExists;
        private String tableComment;
        private String engine;
        private ColumnMeta currentColumn;

        CreateTable(String table, boolean ifNotExists) {
            this.table = table;
            this.ifNotExists = ifNotExists;
        }

        @Override
        public String getSql(boolean pretty) {
            return buildTableMeta().toSql(ifNotExists, pretty);
        }


        public TableMeta buildTableMeta() {
            // Finalize last column
            TableMeta table = new TableMeta();
            table.setName(this.table);
            table.setComment(tableComment);
            table.setEngine(engine);
            table.setColumns(columns.toArray(new ColumnMeta[0]));
            table.setIndexes(indices.toArray(new IndexMeta[0]));
            table.setPrimaryKeys(primaryKeys.toArray(new String[0]));
            return table;
        }

    }

}